--[[--------------------------------------
WORKING NAME: "Cell Infinite Maps"		--
AUTHOR: Philpax, with some extra code	--
from Val, theJ89 and other misc places	--
DESC: A project to bring infinite maps	--
to GMod via a tiling system				--
----------------------------------------]]
local prohibitedEntities = { "func_cell_link", "worldspawn" }
local ENT = FindMetaTable( "Entity" )
local PLY = FindMetaTable( "Player" )
local cell_sv_debug = CreateConVar( "cell_sv_debug", "0", FCVAR_REPLICATED )

--[[--------------------------------------------------
FUNCTION: cells.AddPlayerEnts( player, entities )	--
AUTHOR: Philpax										--
DESC: Gets entities associated with the player 		--
----------------------------------------------------]]
function cells.AddPlayerEnts( player, entities )
	table.insert( entities, player:GetViewModel() )
	
	for _, v in ipairs( player:GetWeapons() ) do table.insert( entities, v ) end
	for _, v in ipairs( ents.FindByClass( "physgun_beam" ) ) do if v:GetOwner() == player then table.insert( entities, v ) end end
	if player:GetVehicle() then	table.insert( entities, player:GetVehicle() ) end
end

--[[--------------------------------------
FUNCTION: ENT:SetCell( cell )			--
AUTHOR: Philpax							--
DESC: Sets the 'cell' of the entity,	--
	including any constrained entities	--
----------------------------------------]]
function ENT:SetCell( cell )
	if IsValid( self ) then
		if table.HasValue( prohibitedEntities, self:GetClass() ) then return end
		
		local entities = {}
		
		for _, v in ipairs( cells.GetConstrainedEntsWithSelf( self ) ) do
			table.insert( entities, v )
		
			if v:IsVehicle() then
				for _, y in pairs( player.GetAll() ) do -- Running GetDriver on a vehicle that hasn't had Spawn() run yet crashes the game. Hacky fix required, but then again this entire thing is a massive hack...
					if y:GetParent() == v then
						table.insert( entities, y )
						cells.AddPlayerEnts( y, entities )
					end
				end
			elseif v:IsPlayer() then
				cells.AddPlayerEnts( v, entities )
			end
			
		end
		
		for _, v in ipairs( entities ) do
			if IsValid( v ) then
				if cell_sv_debug:GetInt() > 1 then
					MsgN( v:GetClass() .. " (" .. v:EntIndex() .. ") -> Cell " .. tostring( cell ) )
				end
				
				v:SetNetworkedInt( "Cell_X", cell.X )
				v:SetNetworkedInt( "Cell_Y", cell.Y )
				v:SetNetworkedInt( "Cell_Z", cell.Z )

				v.Cell = cell

				local sCell = tostring( cell )
				if !Cells[sCell] then
					Cells[sCell] = {}
				end
				
				Cells[sCell][v:EntIndex()] = v
				
				timer.Simple( 0.1, function()
					umsg.Start("CellChange")
						umsg.Entity(v)
						umsg.Vector(cell:ToVector())
					umsg.End()
				end)
			end
		end
	else
		return false
	end
end

--[[------------------------------------------
FUNCTION: ENT:SetCellRepeat( cellRepeat )	--
AUTHOR: Philpax								--
DESC: Sets the entity to repeat across cells--
	Used for common ground in maps, etc		--
--------------------------------------------]]
function ENT:SetCellRepeat( cellRepeat )
	self:SetNetworkedBool( "CellRepeat_X", tobool(cellRepeat[1]) )
	self:SetNetworkedBool( "CellRepeat_Y", tobool(cellRepeat[2]) )
	self:SetNetworkedBool( "CellRepeat_Z", tobool(cellRepeat[3]) )
	self.CellRepeat = cellRepeat
end

function ENT:SetCellRepeatMin( cellRepeat )
	self:SetNetworkedInt( "CellRepeatMin_X", cellRepeat[1] )
	self:SetNetworkedInt( "CellRepeatMin_Y", cellRepeat[2] )
	self:SetNetworkedInt( "CellRepeatMin_Z", cellRepeat[3] )
	self.CellRepeatMin = Cell( cellRepeat[1], cellRepeat[2], cellRepeat[3] )
end

function ENT:SetCellRepeatMax( cellRepeat )
	self:SetNetworkedInt( "CellRepeatMax_X", cellRepeat[1] )
	self:SetNetworkedInt( "CellRepeatMax_Y", cellRepeat[2] )
	self:SetNetworkedInt( "CellRepeatMax_Z", cellRepeat[3] )
	self.CellRepeatMax = Cell( cellRepeat[1], cellRepeat[2], cellRepeat[3] )
end

--[[--------------------------------------
FUNCTION: ENT:SetCellPos( pos )			--
AUTHOR: Philpax							--
DESC: Sets the position of the entity	--
	through a SetPos-like function that	--
	compensates for the cell mechanism	--
----------------------------------------]]
ENT.SetPos_cell = ENT.SetPos
function ENT:SetCellPos( pos )
	local _cell, _pos = cells.AbsoluteToLocal( pos )
	
	if cell_sv_debug:GetInt() > 0 then
		Msg( self:GetClass() .. " (" .. self:EntIndex() .. ") <- " .. tostring(self:GetPos()) .. " (" .. tostring(self:GetCell()) .. ") -> " .. tostring(pos) .. " (" .. tostring(_cell) .. ")\n" )
	end
	
	self:SetCell( _cell )
	self:SetPos_cell( _pos )
end

--[[--------------------------------------------------
FUNCTION: ENT:SetPos( )								--
AUTHOR: Philpax										--
DESC: Let's get funky and override SetPos!			--
----------------------------------------------------]]
function cells.SetPosOverride( self, pos )
	self:SetCellPos( pos )
end
ENT.SetPos = cells.SetPosOverride

concommand.Add( "cell_setpos_override", function(ply,cmd,args) ENT.SetPos = cells.SetPosOverride end)

--[[--------------------------------------
FUNCTION: PLY:AddCount( type, ent )		--
AUTHOR: Philpax / Overv					--
DESC: Overrides AddCount to cover the	--
	creation of entities by the player	--
	that standard hooks cannot intercept--
----------------------------------------]]
local oldCount = PLY.AddCount
function PLY:AddCount( type, ent )
	if ent:GetCell() == nil or cell_origin then ent:SetCell( self:GetCell() ) end
	return oldCount( self, type, ent )
end

--[[--------------------------------------
FUNCTION: cleanup.Add( ply, type, ent )	--
AUTHOR: Philpax / Overv					--
DESC: Overrides cleanup.Add to cover 	--
	creation of entities by the player	--
	that standard hooks cannot intercept--
----------------------------------------]]
local oldCleanup = cleanup.Add
function cleanup.Add( ply, type, ent )
	if ( ent ) then if ent:GetCell() == nil or cell_origin then ent:SetCell( ply:GetCell() ) end end
	return oldCleanup( ply, type, ent )
end

--[[--------------------------------------
FUNCTION: cells.Start()					--
HOOK: InitPostEntity					--
AUTHOR: Philpax							--
DESC: If entities have not been assigned--
	cells by InitPostEntity, set them	--
	to 0, 0, 0							--
----------------------------------------]]
function cells.Start()
	for _, v in ipairs( ents.GetAll() ) do
		if IsValid( v ) then
			if v:GetCell() == nil then -- Make sure the entity is valid and it doesn't already have a cell, otherwise we're quickly going towards a crash / impossible to map for
				v:SetCell( cell_origin )
			end

			if v:GetCellRepeat() == nil then
				v:SetCellRepeat( {false,false,false} )
			end

			if v:GetCellRepeatMin() == nil then
				v:SetCellRepeatMin( cell_origin )
			end

			if v:GetCellRepeatMax() == nil then
				v:SetCellRepeatmax( cell_origin )
			end
		end
	end
end
hook.Add( "InitPostEntity", "CellStart", cells.Start )

--[[----------------------------------------------
FUNCTION: cells.EntKeyValue( ent, key, val )	--
HOOK: EntityKeyValue							--
AUTHOR: Philpax									--
DESC: Assign cells to entities that have		--
	keyvalues specifying their cell				--
------------------------------------------------]]
function cells.EntKeyValue( ent, key, val )
	if key == "cell" then 
		ent:SetCell( Cell( unpack( val:Split(" ") ) ) ) 
	end
	
	if key == "cell_repeat" then
		ent:SetCellRepeat( val:Split(" ") )
	elseif key == "cell_repeat_min" then
		ent:SetCellRepeat( { false, false, false } )
		ent:SetCellRepeatMin( val:Split(" ") )
	elseif key == "cell_repeat_max" then
		ent:SetCellRepeat( { false, false, false } )
		ent:SetCellRepeatMax( val:Split(" ") )
	end
end
hook.Add( "EntityKeyValue", "CellKeyValue", cells.EntKeyValue )

--[[--------------------------------------
FUNCTION: cells.PlayerSpawn( ply )		--
HOOK: PlayerSpawn						--
AUTHOR: Philpax							--
DESC: Ensure the player spawns in the	--
	same cell as their info_player_start--
----------------------------------------]]
function cells.PlayerSpawn( ply ) 
	local entities = ents.FindByClass("info_player_start")
	for _, v in ipairs( entities ) do
		if v:GetPos_cell():Distance(ply:GetPos_cell()) < 4 then
			ply:SetCell( v:GetCell() ) 
		end
	end
end

hook.Add( "PlayerSpawn", "CellPlayerSpawn", cells.PlayerSpawn )
hook.Add( "PlayerLoadout", "CellLoadout", function( ply ) ply:SetCell( ply:GetCell() ) end)

-- Various fixes
hook.Add( "PhysgunPickup", "CellPickup", function( ply, ent ) if ent:GetClass() == "func_brush" then return false end ent:SetCell( ply:GetCell() ) end)
hook.Add( "EntityTakeDamage", "CellTakeDamage", function( ent, inf, att, amount, dmginfo ) if ent:GetCell() != att:GetCell() then dmginfo:SetDamage( 0 ) end end)
hook.Add( "OnEntityCreated", "CellOwnerFix", function( ent ) 
	if IsValid(ent) then 
		if ent:GetOwner():IsValid() then 
			ent:SetCell( ent:GetOwner():GetCell() ) 
		elseif ent:GetParent():IsValid() then
			ent:SetCell( ent:GetParent():GetCell() )
		end 
	end 
end)

-- Cell switch concommands
concommand.Add( "cell_switch", function(ply,cmd,args) ply:SetCell( Cell( tonumber(args[1]), tonumber(args[2]), tonumber(args[3]) ) ) end)
concommand.Add( "cell_switchent", function(ply,cmd,args) ply:GetEyeTrace().Entity:SetCell(Cell( tonumber(args[1]), tonumber(args[2]), tonumber(args[3]) )) end)
concommand.Add( "cell_broadcast", function(ply,cmd,args) for _, v in pairs( player.GetAll() ) do ply:PrintMessage( HUD_PRINTTALK, v:Nick() .. " is in cell " .. tostring( v:GetCell() ) ) end end)
